package network;

/**
 * Created with IntelliJ IDEA.
 * Description:
 * User: Ap0stoL2
 * Date: 2023-08-11
 * Time: 11:48
 */


import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class TcpEchoServer {


    private ServerSocket serverSocket = null;
    // 此处不应该创建固定线程数目的线程池.
    private ExecutorService service = Executors.newCachedThreadPool();//线程池

    //构造方法
    // 这个操作就会绑定端口号
    public TcpEchoServer(int port) throws IOException {
        serverSocket = new ServerSocket(port);
    }


    // 启动服务器
    public void start() throws IOException {
        System.out.println("服务器启动!");//打印日志


        //服务器启动后在while循环时,第一步不是接收客户端请求,而是与客户端连接
        while (true) {

            //把内核中已连接的对象获取到应用程序中了,这个过程类似于生产者消费者模型
            //返回值是一个Socket
            Socket clientSocket = serverSocket.accept();


//***************************************************************************************************************
            //问题2:
            // 单个线程,不能处理多个服务器请求,只能一个结束才能处理下一个
            // 多线程解决此问题,在主线程建立连接,在主线程建立连接完后,创建新线程专门用于处理客户端请求
//            Thread t = new Thread(() -> {
//                processConnection(clientSocket);
//            });
//            t.start();


            //问题1:
            // 这个写法,是能自动关闭close,只要实现 Closeable 接口就可以这么写.
            //这么写会有其他问题.processConnection和主线程是不同线程,执行processConnection的过程中,主线程的try就执行完毕了,这就导致clientSocket没用完就被关闭了
//            try(Socket clientSocket = serverSocket.accept();){
//                Thread t = new Thread(() -> {
//                    processConnection(clientSocket);
//                });
//                t.start();
//
//            };



            // 使用线程池, 来解决上述问题
            service.submit(new Runnable() {
                @Override
                public void run() {
                    processConnection(clientSocket);
                }
            });
//***************************************************************************************************************


        }
    }

    // 通过这个方法来处理一个连接的逻辑.
    private void processConnection(Socket clientSocket) {
        System.out.printf("[%s:%d] 客户端上线!\n", clientSocket.getInetAddress().toString(), clientSocket.getPort());


        // 接下来就可以1.读取请求, 2.根据请求计算响应, 3.返回响应三步走了.
        // Socket 对象内部包含了两个字节流对象, 可以把这俩字节流对象获取到, 完成后续的读写工作
        try (InputStream inputStream = clientSocket.getInputStream();
             OutputStream outputStream = clientSocket.getOutputStream()) {

            // 一次连接中, 可能会涉及到多次请求/响应
            while (true) {

                // 1. 读取请求并解析. 为了读取方便, 直接使用 Scanner.(把字节流在内地转化为字符流)
                Scanner scanner = new Scanner(inputStream);
                //当没读到客户端数据的时候hasNext会一直阻塞,或者客户端退出,hashNext感知到客户端退出就break了
                if (!scanner.hasNext()) { //如果读到eof,就读完了,就退出循环
                    // 读取完毕, 客户端下线.
                    System.out.printf("[%s:%d] 客户端下线!\n", clientSocket.getInetAddress().toString(), clientSocket.getPort());
                    break;
                }

                // 这个代码暗含一个约定, 客户端发过来的请求得是文本数据, 同时还得带有空白符作为分割. (比如换行这种)
                //next就是一直读数据,直到读到空白符结束(空白符是一类字符,包括不限于: 换行'\n',回车'\r',空格,制表符,换页符,垂直制表符)
                //换行('\n')是让光标到下一行,回车('\r')是让光标回到行首,不会另起一行
                String request = scanner.next();



                // 2. 根据请求计算响应
                String response = process(request);



                // 3. 把响应写回给客户端. 把 OutputStream 使用 PrinterWriter 包裹一下, 方便进行发数据.
                PrintWriter writer = new PrintWriter(outputStream);
                //使用 PrintWriter 的 println 方法, 把响应返回给客户端.
                //此处用 println, 而不是 print 就是为了在结尾加上 \n . 方便客户端读取响应, 使用 scanner.next 读取.
                writer.println(response);
                //这里还需要加一个 "刷新缓冲区" 操作.
                //IO操作是比较有开销的相比于访问内存,进行IO次数越多,程序越慢
                //为了提高效率,减少IO次数,使用一块内存充当缓冲区,写数据先写到缓冲区里,攒一波再进行IO,flush就可以刷新缓冲区,确保数据真的通过网卡发了出去,而不是残留在内存缓冲区
                //上述说的是全缓冲,而换行('\n')刷新是行缓冲,行缓冲通常用于标准输入输出(控制台上打印和输入我们通常输入enter linux本质就是'\n',windows 是 '\r\n'),//但我们现在是往网卡/网络文件中写(全缓冲,不会受到'\n'影响)
                writer.flush();


                // 日志, 打印当前的请求详情.
                System.out.printf("[%s:%d] req: %s, resp: %s\n", clientSocket.getInetAddress().toString(), clientSocket.getPort(),
                        request, response);
            }
        } catch (IOException e) {
            e.printStackTrace();//捕获try的IO异常
        } finally {
            // 在 finally 中加上 close 操作, 确保当前 socket 被及时关闭!!
            try {
                clientSocket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }


    public String process(String request) {
        return request;
    }

    public static void main(String[] args) throws IOException {
        TcpEchoServer server = new TcpEchoServer(9090);
        server.start();
    }
}